spb/cancerindex
Public
TypeScript 97.2%
SQL 1.5%
CSS 0.6%
JavaScript 0.5%
1import type { Metadata } from 'next';2import Link from 'next/link';3import { notFound } from 'next/navigation';4import { ExternalLink } from 'lucide-react';5import { PageHeader, Section, KV, Note } from '@/components/ui/section';6import { Badge, StatusBadge } from '@/components/ui/badge';7import { EmptyState } from '@/components/ui/empty-state';8import { Freshness } from '@/components/ui/freshness';9import { getSourceBySlug, listRuns, recordCountsByKind, licenseMeaning, sourceDomains } from '@/lib/queries/sources';10import { fmtDate, fmtDateTime, fmtDuration, fmtInt, humanize, relativeTime } from '@/lib/format';1112export const revalidate = 300;1314export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {15 const s = await getSourceBySlug((await params).slug);16 return s ? { title: `${s.name} — source`, description: s.description ?? `License, access, connector health and record counts for ${s.name}.` } : { title: 'Source' };17}1819export default async function SourcePage({ params }: { params: Promise<{ slug: string }> }) {20 const { slug } = await params;21 const s = await getSourceBySlug(slug);22 if (!s) notFound();23 const [runs, kinds] = await Promise.all([listRuns({ sourceId: s.id, limit: 30 }), recordCountsByKind(s.id)]);24 const domains = [...sourceDomains(s)];2526 return (27 <div>28 <PageHeader kicker={`Source · ${humanize(s.category)}`} title={s.name} lede={s.description ?? undefined}>29 <p className="mt-2 flex flex-wrap items-center gap-2 text-[12.5px]">30 <span className="ci-mono text-ink-3">{s.id}</span>31 <span className="ci-mono text-ink-3">connector: {s.slug}</span>32 <StatusBadge status={s.status} />33 <StatusBadge status={s.health ?? 'unknown'} />34 {s.homepage ? (35 <a className="ci-link inline-flex items-center gap-1" href={s.homepage} target="_blank" rel="noopener noreferrer">36 Homepage <ExternalLink className="h-3 w-3" aria-hidden />37 </a>38 ) : null}39 {s.docs_url ? (40 <a className="ci-link inline-flex items-center gap-1" href={s.docs_url} target="_blank" rel="noopener noreferrer">41 API docs <ExternalLink className="h-3 w-3" aria-hidden />42 </a>43 ) : null}44 {s.terms_url ? (45 <a className="ci-link inline-flex items-center gap-1" href={s.terms_url} target="_blank" rel="noopener noreferrer">46 Terms <ExternalLink className="h-3 w-3" aria-hidden />47 </a>48 ) : null}49 </p>50 </PageHeader>5152 <div className="grid gap-8 lg:grid-cols-[1fr_360px]">53 <div className="space-y-8">54 <Section id="license" kicker="License" title="License and attribution">55 <KV56 items={[57 { k: 'Status', v: <Badge tone={s.license_status === 'approved' ? 'ok' : s.license_status === 'blocked' ? 'danger' : 'warn'}>{s.license_status}</Badge> },58 { k: 'License', v: s.license ?? <span className="text-ink-3">not recorded</span> },59 { k: 'Commercial use', v: humanize(s.commercial_use) },60 { k: 'Redistribution', v: humanize(s.redistribution) },61 { k: 'Reviewed', v: s.license_reviewed_at ? fmtDate(s.license_reviewed_at) : <span className="text-ink-3">not yet reviewed</span> },62 { k: 'Approved for production', v: s.approved_for_production ? 'Yes' : 'No' },63 { k: 'Attribution', v: s.attribution ? <span className="italic">{s.attribution}</span> : <span className="text-ink-3">—</span> },64 ]}65 />66 <Note>{licenseMeaning(s.license_status, s.redistribution, s.commercial_use)}</Note>67 </Section>6869 <Section id="runs" kicker="Connector" title="Ingest runs" description="Every execution of the connector: mode, status, counts, duration and dataset version. Runs never mass-delete on a shrunken response (anomaly guard).">70 {runs.length ? (71 <div className="ci-table-wrap">72 <table className="ci-table">73 <thead>74 <tr>75 <th>Run</th>76 <th>Mode</th>77 <th>Status</th>78 <th>Started</th>79 <th className="num">Duration</th>80 <th className="num">Fetched</th>81 <th className="num">Created</th>82 <th className="num">Updated</th>83 <th className="num">Rejected</th>84 <th className="num">HTTP fail</th>85 <th>Dataset version</th>86 </tr>87 </thead>88 <tbody>89 {runs.map((r) => (90 <tr key={r.id}>91 <td>92 <Link href={`/admin/runs/${r.id}`} className="ci-mono ci-link text-[11.5px]">93 {r.id}94 </Link>95 {r.anomaly ? <Badge tone="danger" className="ml-1">anomaly</Badge> : null}96 {r.schema_drift?.length ? <Badge tone="warn" className="ml-1">drift {r.schema_drift.length}</Badge> : null}97 </td>98 <td>{r.mode}</td>99 <td>100 <StatusBadge status={r.status} />101 </td>102 <td className="whitespace-nowrap text-[12.5px]">{fmtDateTime(r.started_at)}</td>103 <td className="num">{fmtDuration(r.duration_ms)}</td>104 <td className="num">{fmtInt(r.records_fetched)}</td>105 <td className="num">{fmtInt(r.records_created)}</td>106 <td className="num">{fmtInt(r.records_updated)}</td>107 <td className="num">{fmtInt(r.records_rejected)}</td>108 <td className="num">109 {fmtInt(r.http_failures)}/{fmtInt(r.http_requests)}110 </td>111 <td className="ci-mono text-[11.5px]">{r.dataset_version ?? '—'}</td>112 </tr>113 ))}114 </tbody>115 </table>116 </div>117 ) : (118 <EmptyState compact title="No run yet">119 {s.status === 'awaiting_credentials' ? 'The connector is waiting for API credentials.' : s.status === 'review' ? 'The connector is blocked until the license review completes.' : 'This connector has not been executed on this environment.'}120 </EmptyState>121 )}122 </Section>123124 <Section id="records" kicker="Records" title="Source records by entity kind" description="Native records kept for idempotency and lineage (source + entity kind + source record id).">125 {kinds.length ? (126 <div className="ci-table-wrap">127 <table className="ci-table">128 <thead>129 <tr>130 <th>Entity kind</th>131 <th>Status</th>132 <th className="num">Records (count)</th>133 <th>Last retrieved</th>134 </tr>135 </thead>136 <tbody>137 {kinds.map((k) => (138 <tr key={`${k.entity_kind}-${k.status}`}>139 <td className="ci-mono">{k.entity_kind}</td>140 <td>141 <StatusBadge status={k.status} />142 </td>143 <td className="num">{fmtInt(k.n)}</td>144 <td className="whitespace-nowrap text-[12.5px]">{k.last_retrieved ? fmtDateTime(k.last_retrieved) : '—'}</td>145 </tr>146 ))}147 </tbody>148 </table>149 </div>150 ) : (151 <EmptyState compact>No record ingested from this source yet.</EmptyState>152 )}153 <Freshness dataUpdatedAt={s.last_success_at} sourceVersion={s.last_dataset_version} />154 </Section>155 </div>156157 <aside className="space-y-8">158 <Section id="access" kicker="Access" title="Connector profile" level={3}>159 <KV160 items={[161 { k: 'Organization', v: s.organization },162 { k: 'Access', v: `${s.access_type} · ${humanize(s.access_auth)}` },163 { k: 'Update frequency', v: s.update_frequency },164 { k: 'Incremental', v: s.supports_incremental ? 'Yes' : 'No (full sync)' },165 { k: 'Rate limit', v: s.rate_limit },166 { k: 'Tier', v: <span className="ci-num">{s.tier}</span> },167 { k: 'Entities', v: s.entities.length ? s.entities.join(', ') : null },168 { k: 'Metrics', v: s.metrics.length ? s.metrics.join(', ') : null },169 { k: 'Domains', v: domains.length ? domains.join(', ') : null },170 ]}171 />172 </Section>173 <Section id="health" kicker="Health" title="Connector health" level={3}>174 <KV175 items={[176 { k: 'Health', v: <StatusBadge status={s.health ?? 'unknown'} /> },177 { k: 'Detail', v: s.health_detail },178 { k: 'Paused', v: s.paused ? 'Yes' : 'No' },179 { k: 'Last success', v: s.last_success_at ? `${relativeTime(s.last_success_at)} (${fmtDateTime(s.last_success_at)})` : '—' },180 { k: 'Last attempt', v: s.last_attempt_at ? fmtDateTime(s.last_attempt_at) : '—' },181 { k: 'Last run', v: s.last_run_id ? <Link className="ci-mono ci-link text-[11.5px]" href={`/admin/runs/${s.last_run_id}`}>{s.last_run_id}</Link> : '—' },182 ]}183 />184 </Section>185 </aside>186 </div>187 </div>188 );189}190